/**
 * Copyright 2009 Sergio Bossa
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.googlecode.actorom.impl.core;

import com.googlecode.actorom.Address;
import com.googlecode.actorom.KillActorException;
import com.googlecode.actorom.Topology;
import com.googlecode.actorom.annotation.AddressInstance;
import com.googlecode.actorom.annotation.OnKill;
import com.googlecode.actorom.annotation.OnMessage;
import com.googlecode.actorom.annotation.OnSpawn;
import com.googlecode.actorom.annotation.TopologyInstance;
import com.googlecode.actorom.impl.core.reactor.MessageHandler;
import com.googlecode.actorom.support.ActorCreationException;
import com.googlecode.actorom.support.ActorInvocationException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import net.jcip.annotations.NotThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Sergio Bossa
 */
@NotThreadSafe
public class CoreProcess implements MessageHandler {

    private static final transient Logger LOG = LoggerFactory.getLogger(CoreProcess.class);
    private final Map<Class, Method> onMessageMethods = new HashMap<Class, Method>();
    private final Topology topology;
    private final Address address;
    private final Object handler;
    private volatile boolean active;

    CoreProcess(Topology topology, Address address, Object handler) {
        this.topology = topology;
        this.address = address;
        this.handler = handler;
        active = false;
        injectTopologyInstance();
        injectAddressInstance();
        cacheOnMessageMethods();
    }

    public void spawn() {
        if (!active) {
            try {
                Method onSpawnMethod = getOnSpawnMethod();
                if (onSpawnMethod != null) {
                    onSpawnMethod.setAccessible(true);
                    onSpawnMethod.invoke(handler, new Object[0]);
                }
                active = true;
            } catch (Exception ex) {
                LOG.error("Error while invoking on spawn method for handler of type {}", handler.getClass());
                throw new ActorCreationException(ex.getMessage(), ex);
            }
        } else {
            throw new IllegalStateException("Actor already spawned!");
        }
    }

    public boolean isActive() {
        return active;
    }

    public Address getAddress() {
        return address;
    }

    public void handle(Object message) {
        try {
            LOG.debug("Handler of type {} invoked on message: {}", handler.getClass(), message);
            Method invokable = getOnMessageMethodFor(message);
            if (invokable != null) {
                LOG.debug("Found method {} for message: {}", invokable, message);
                Class[] types = invokable.getParameterTypes();
                Object[] arguments;
                if (types.length == 1 && types[0].isAssignableFrom(message.getClass())) {
                    arguments = new Object[]{message};
                } else {
                    arguments = new Object[0];
                }
                invokable.setAccessible(true);
                try {
                    invokable.invoke(handler, arguments);
                } catch (InvocationTargetException ex) {
                    Throwable cause = ex.getCause();
                    if (cause instanceof KillActorException) {
                        active = false;
                        Method onKillMethod = getOnKillMethod();
                        if (onKillMethod != null) {
                            onKillMethod.setAccessible(true);
                            onKillMethod.invoke(handler, new Object[0]);
                        }
                    } else {
                        throw ex;
                    }
                }
            } else {
                throw new ActorInvocationException("Cannot invoke handler for message of type " +
                        message.getClass() +
                        ": no suitable method found for handler of type " +
                        handler.getClass());
            }
        } catch (Exception ex) {
            LOG.error("Error while executing handler of type {} on message of type {}", handler.getClass(), message.getClass());
            throw new ActorInvocationException(ex.getMessage(), ex);
        }
    }

    private void injectAddressInstance() {
        try {
            Field[] fields = handler.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(AddressInstance.class)) {
                    field.setAccessible(true);
                    field.set(handler, address);
                }
            }
        } catch (Exception ex) {
            LOG.warn("Unable to inject address instance.");
            LOG.warn(ex.getMessage(), ex);
        }
    }

    private void injectTopologyInstance() {
        try {
            Field[] fields = handler.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(TopologyInstance.class)) {
                    field.setAccessible(true);
                    field.set(handler, topology);
                }
            }
        } catch (Exception ex) {
            LOG.warn("Unable to inject topology instance.");
            LOG.warn(ex.getMessage(), ex);
        }
    }

    private void cacheOnMessageMethods() {
        Method[] methods = handler.getClass().getMethods();
        for (Method method : methods) {
            OnMessage onMessage = method.getAnnotation(OnMessage.class);
            if (onMessage != null) {
                Class messageType = onMessage.type();
                onMessageMethods.put(messageType, method);
            }
        }
    }

    private Method getOnSpawnMethod() {
        Method[] methods = handler.getClass().getMethods();
        Method onSpawnMethod = null;
        for (Method method : methods) {
            if (method.isAnnotationPresent(OnSpawn.class) && method.getParameterTypes().length == 0) {
                onSpawnMethod = method;
            }
        }
        return onSpawnMethod;
    }

    private Method getOnMessageMethodFor(Object message) {
        Method onMessageMethod = null;
        Class messageType = message.getClass();
        do {
            onMessageMethod = onMessageMethods.get(messageType);
            if (onMessageMethod == null) {
                messageType = messageType.getSuperclass();
            }
        } while (onMessageMethod == null && messageType != null);
        return onMessageMethod;
    }

    private Method getOnKillMethod() {
        Method[] methods = handler.getClass().getMethods();
        Method onKillMethod = null;
        for (Method method : methods) {
            if (method.isAnnotationPresent(OnKill.class) && method.getParameterTypes().length == 0) {
                onKillMethod = method;
            }
        }
        return onKillMethod;
    }
}
